iT邦幫忙

1

從 Linux 基礎實現 Docker Bridge 網路:一步步理解容器通訊 (3)

  • 分享至 

  • xImage
  •  

部落格好讀版


在上一章中,我們成功讓容器之間的封包可以正常傳遞,但容器對外的封包傳遞問題仍未解決。本章我們將逐步探討這個問題,首先來檢查 ns1 到 root namespace 之間可能缺少的部分。

回顧

讓我們重新回憶上次的操作和訊息:

# 從 ns1 ping Host IP
$ sudo ip netns exec ns1 ping -c 2 172.31.39.53
###
ping: connect: Network is unreachable
######
# 從 Host ping 位於 ns0 的 veth0 IP
$ ping -c 2 172.18.0.2
###
PING 172.18.0.2 (172.18.0.2) 56(84) bytes of data.
^C
--- 172.18.0.2 ping statistics ---
2 packets transmitted, 0 received, 100% packet loss, time 1004ms

ping: connect: Network is unreachable 這個錯誤表示你的系統無法找到通往目標網路的路徑。這意味著:

  1. 網路不通:系統無法與目標設備或網路建立連接,可能是因為路由設定錯誤、網路接口未啟動,或者目標設備不可達。
  2. 路由表 (Routing Table) 問題:系統的路由表可能沒有正確的路由條目來轉發流量到目標網路,或者預設路由設定有問題。

簡單來說,這訊息就是告訴你:「我想去那個地方,但是找不到路。」。

什麼是 Routing Table

路由表(Routing Table)是一張指導網路流量(數據包)如何從源地址送到目標地址的地圖。它告訴系統每一個目標網段應該經由哪條路徑、哪個網關(下一跳)或哪個網路接口來到達目的地。

我認為可以用早期的電話中心(Call Center)來理解路由表。每次打電話需要人工接線員將你的線路插到對方的端口,讓兩人通話。路由表就像這個接線員的「電話分配表」,決定每條線路(數據包)應該接到哪個插孔(下一跳)。

  • 路由表 就是接線員的指導工具,幫助接線員快速找到正確的插孔或轉發路徑。
  • 查表決定路徑:接線員(系統)依據路由表,找到數據包(電話)應該送到的下一個接口(插孔)。
  • 直接連接更快:如果兩個電話在同一交換機(網段內),接線員能直接連接,不需要轉發,效率最高。
  • 處理默認情況:當找不到具體的匹配(例如國際長途),接線員將按默認規則轉接到大總機(默認路由)。

路由表的類型

依照前面提到的執行規則,路由表的類型通常可分為以下兩類:

  • 內網通信 (Directly connected route):如果目標在同一網段(例如 192.168.1.0/24),數據包會直接發送到目標,不需要經過網關。
  • 外網通信 (Remote routes):如果目標地址不在內網範圍,數據包會發送到默認網關,由它負責轉發到外部網路。

Routing Table 的條目組成

每一條路由條目通常會包括:

  • Destination:目標網路或 IP 範圍,例如 192.168.1.0/24(內網)或 0.0.0.0/0(所有未知目標)。
  • Gateway:將封包轉發到下一跳的路由器或設備的 IP。例如默認路由(0.0.0.0)通常經由網關 192.168.1.1
  • Genmask:確定目標網段的大小,例如:
    • 255.255.255.0(/24)
    • 255.255.0.0(/16)
  • Flags
    • U:表示這條路由是「啟用」的。
    • G:表示這條路由是指向 gateway(即需要轉發的路由)。
  • Metric:路由的優先順序,數字越小優先級越高。
  • Iface:使用哪個網路介面(例如 eth0 或 wlan0)來發送封包。

如何查詢 Routing Table

在 Linux 中,可以使用 routeip route 命令來查看路由表。這些命令會顯示系統如何轉發網路封包,尤其是選擇哪些路由來到達不同的目標網路。

我覺得 route 指令比較好理解,不過 Ubuntu 並沒有預先安裝 route,可以使用以下指令安裝:

apt-get update
apt-get install net-tools

查詢 routing table:

route -n

-n 代表顯示 IP 數字,而不要顯示 hostnames。

查詢結果類似如下:

Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         192.168.1.1     0.0.0.0         UG    100    0        0 eth0
192.168.1.0     0.0.0.0         255.255.255.0   U     0      0        0 eth0
10.0.0.0        192.168.1.254   255.255.255.0   UG    200    0        0 eth0
172.16.0.0      0.0.0.0         255.255.0.0     U     0      0        0 docker0

路由表的解讀邏輯

  • 如果你要送信到 192.168.1.23(同一個社區),路由表會說:「不需要經過網關,直接送到社區內的地址,使用 eth0」。
  • 如果你要送信到 10.0.0.45(另一個城市),路由表會說:「找到中轉站 192.168.1.254,經過它送信,使用 eth0」。
  • 如果地址是其他地方(例如 8.8.8.8),路由表會說:「不知道怎麼處理,發給默認網關 192.168.1.1 來處理,使用 eth0」。

觀察 ns1 路由表

我們先來觀察 ns1 路由表的內容:

$ sudo ip netns exec ns1 route -n
###
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
172.18.0.0      0.0.0.0         255.255.255.0   U     0      0        0 veth1

依照上面我們看到的邏輯,當我們 ping 另一個容器 (172.18.0.2) 時,封包會依照這個條目,透過 veth1 送出去,經由 veth pair 到另一端網路介面 veth1-br,再透過 bridge 傳送到 veth0-br,然後又透過 veth pair 送到另一端的 veth0。這也是為什麼容器與容器間的傳送是暢通的原因。

我們可以使用 tracepath 來追蹤網路封包的路徑(預設使用 UDP 封包),它簡單並且不需要額外的權限,適合快速排查。不過需要注意的是,tracepath 無法追蹤 ICMP 封包,這限制了它對某些情況的適用性。

$ sudo ip netns exec ns1 tracepath -n 172.18.0.2
 1?: [LOCALHOST]                      pmtu 1500
 1:  172.18.0.2                                            0.063ms reached
 1:  172.18.0.2                                            0.042ms reached
     Resume: pmtu 1500 hops 1 back 2

但很顯然的,當我們的目標是 Host IP (172.31.39.53) 時,這張表就無法指引我們該往哪裡去,因此就失敗了。

sudo ip netns exec ns1 tracepath -n 172.31.39.53
 1:  send failed
     Resume: pmtu 65535

查看 root namespace 路由表

$ route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         172.31.32.1     0.0.0.0         UG    512    0        0 enX0
172.17.0.0      0.0.0.0         255.255.0.0     U     0      0        0 docker0
172.31.0.2      172.31.32.1     255.255.255.255 UGH   512    0        0 enX0
172.31.32.0     0.0.0.0         255.255.240.0   U     512    0        0 enX0
172.31.32.1     0.0.0.0         255.255.255.255 UH    512    0        0 enX0

我們來分析開頭提到的案例,"從 Host ping 位於 ns0 的 veth0 IP (172.18.0.2)",會發生什麼事。

  1. 由於 172.18.0.2 不被其他 Destination 條目選中,所以它跳到了條目 1,走網關 172.31.32.1
  2. 來到條目 5,不需要轉發 (沒有 flag G),因此往外網送了,但外網並沒有 172.18.0.2 這個 IP,因此得不到回應,就卡在這裡。
$  tracepath -n -m 10 172.18.0.2
 1?: [LOCALHOST]                      pmtu 9001
 1:  no reply
 2:  no reply
 3:  no reply
 4:  no reply
 5:  no reply
 6:  no reply
 7:  no reply
 8:  no reply
 9:  no reply
10:  no reply
     Too many hops: pmtu 9001
     Resume: pmtu 9001

調整路由表

接下來我們要透過調整路由表,來解決剛剛論證的兩個問題:

  1. 從 Host ping 位於 ns0 的 veth0 IP (172.18.0.2) 的連線沒有回應。
  2. 從 ns1 ping Host IP (172.31.39.53) 的連線無法到達。

解決 Host 到 ns0 的問題

第一個問題出乎意料的好解決,只需要為 docker1 這個網路介面加上可以覆蓋 vth0, vth1 的 IP 網段:

sudo ip addr add 172.18.0.1/24 dev docker1

查詢 docker1 的 iptable:

$ ip -br addr
###
lo               UNKNOWN        127.0.0.1/8 ::1/128
enX0             UP             172.31.39.53/20 metric 512 fe80::49d:2ff:fe88:529d/64
docker0          DOWN           172.17.0.1/16
docker1          UP             172.18.0.1/24 fe80::f8b6:c0ff:fe55:3371/64
veth0-br@if6     UP             fe80::7c56:1aff:fe0a:5c8b/64
veth1-br@if8     UP             fe80::3065:8eff:fe22:180/64

還記得上面提到的 Directly connected route 嗎?

  1. 當某個網路介面(interface)有一個 IP 地址時,該網路介面就可以直接訪問與該 IP 屬於同一網段的其他設備。

  2. 自動添加的直連路由條目只會在以下情況發生:

    • IP 地址包含有子網資訊(例如 /24 的子網掩碼)。
    • 介面是啟動狀態(ip link set <interface> up)。

Linux 的內核網路堆疊實現了這個自動行為:當你使用 ip addr add 添加 IP 時,內核會同步更新路由表:

$ route -n
###
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         172.31.32.1     0.0.0.0         UG    512    0        0 enX0
172.17.0.0      0.0.0.0         255.255.0.0     U     0      0        0 docker0
172.18.0.0      0.0.0.0         255.255.255.0   U     0      0        0 docker1 <- 更新的條目
172.31.0.2      172.31.32.1     255.255.255.255 UGH   512    0        0 enX0
172.31.32.0     0.0.0.0         255.255.240.0   U     512    0        0 enX0
172.31.32.1     0.0.0.0         255.255.255.255 UH    512    0        0 enX0

現在我們就可以從 Host 與 veth0 建立連線了:

$ ping -c 2 172.18.0.2
PING 172.18.0.2 (172.18.0.2) 56(84) bytes of data.
64 bytes from 172.18.0.2: icmp_seq=1 ttl=127 time=0.057 ms
64 bytes from 172.18.0.2: icmp_seq=2 ttl=127 time=0.041 ms

--- 172.18.0.2 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1047ms
rtt min/avg/max/mdev = 0.041/0.049/0.057/0.008 ms
$ tracepath -n 172.18.0.2
 1?: [LOCALHOST]                      pmtu 1500
 1:  172.18.0.2                                            0.045ms reached
 1:  172.18.0.2                                            0.009ms reached
     Resume: pmtu 1500 hops 1 back 2

解決 ns1 到 Host 的問題

如果我們仔細對比 root namespace 和 ns0, ns1 的路由表,就會發現: ns0, ns1 似乎沒有預設路由。

回想一下,我們在 root namespace 中,是有看到預設條目的:

0.0.0.0         172.31.32.1     0.0.0.0         UG    512    0        0 enX0

我們在本機的 WSL 環境,開啟一個新的 container 來比較一下:

$ docker container run --rm -it alpine /bin/sh
/ # route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         172.17.0.1      0.0.0.0         UG    0      0        0 eth0
172.17.0.0      0.0.0.0         255.255.0.0     U     0      0        0 eth0
/ # ip route
default via 172.17.0.1 dev eth0
172.17.0.0/16 dev eth0 scope link  src 172.17.0.2
/ # ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
11: eth0@if12: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever

因此第二個問題的解決方法,就是要為 ns0, ns1 補上缺失的預設條目。指向 docker1 這個網路介面 IP:

# 添加預設條目 - ns0
sudo ip netns exec ns0 ip route add default via 172.18.0.1
# 添加預設條目 - ns1
sudo ip netns exec ns1 ip route add default via 172.18.0.1

查詢路由表:

# ns0
$ sudo ip netns exec ns0 ip route
###
default via 172.18.0.1 dev veth0
172.18.0.0/24 dev veth0 proto kernel scope link src 172.18.0.2
######
# ns1
$ sudo ip netns exec ns1 ip route
###
default via 172.18.0.1 dev veth1
172.18.0.0/24 dev veth1 proto kernel scope link src 172.18.0.3

測試結果:

# ns0
$ sudo ip netns exec ns0 ping -c 2 172.31.39.53
###
PING 172.31.39.53 (172.31.39.53) 56(84) bytes of data.
64 bytes from 172.31.39.53: icmp_seq=1 ttl=127 time=0.076 ms
64 bytes from 172.31.39.53: icmp_seq=2 ttl=127 time=0.046 ms

--- 172.31.39.53 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 0.046/0.061/0.076/0.015 ms
######
# ns1
$ sudo ip netns exec ns1 ping -c 2 172.31.39.53
###
PING 172.31.39.53 (172.31.39.53) 56(84) bytes of data.
64 bytes from 172.31.39.53: icmp_seq=1 ttl=127 time=0.052 ms
64 bytes from 172.31.39.53: icmp_seq=2 ttl=127 time=0.045 ms

--- 172.31.39.53 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1065ms
rtt min/avg/max/mdev = 0.045/0.048/0.052/0.003 ms

小結

這樣一來,我們已經解決了 bridge 之間的封包傳遞問題,至於容器與外部網路的溝通部分,留到下一章再說。我們下次見。

參考


圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言